import { Injectable, Inject } from '@angular/core';
import { Observable } from 'rxjs/observable';
import { Subscription } from 'rxjs/Subscription';


import { TetriminoCmp } from '../cmps/tetrimino.cmp';
import { MatrixCmp } from '../cmps/matrix.cmp';
import { KeyDownBuffer } from './keydown-buffer';
import { Dimension, Coord, DIMENSION, PREVIEWPOS } from '../models/dimension';
import { Direction } from '../models/direction';
import { Shape } from '../models/shape';
import { Rotation } from '../models/rotation';
import { PieceBag } from './piece-bag';

export enum GameState {
  Uninitialized, Running, Paused, Over
}

@Injectable()
export class GameSvc {

  gameOver: boolean;
  level: number = 1;
  score: number = 0;
  lines: number = 0;

  current: TetriminoCmp;
  inHold: Shape;
  swapped: boolean;
  swapFoul: boolean;

  next: Shape;
  inQueue: Shape[] = [];


  matrixCmp: MatrixCmp;
  paused: boolean;
  landed: boolean;
  speed: number = 2000;

  get state() {
    return !this.matrixCmp ? GameState.Uninitialized :
      this.gameOver ? GameState.Over :
        !this.paused ? GameState.Running :
          GameState.Paused;
  }

  keydownSubscription: Subscription;

  get dropSpot() { return <Coord>[this.dim[0] / 2 - 1, 0]; }


  ///Auto-moving state
  _autoMoveHandle: any = null;
  _autoMoveTick = 80;
  lastKey: string;
  autoMoving: boolean;

  intervalHandle: any = null;
  keyBuffer: KeyDownBuffer = new KeyDownBuffer();

  _autoFallHandle: any = null;

  constructor(
    private pieceBag: PieceBag,
    @Inject(DIMENSION) private dim: Dimension
  ) { }


  start(
    matrix: MatrixCmp,
    piece: TetriminoCmp
  ) {
    this.matrixCmp = matrix;
    // matrix.rearrange.subscribe(this.onRearrange.bind(this));
    this.current = piece;
    matrix.ngOnInit();

    this.next = null;
    this.inQueue = [];

    this.gameOver = null;
    this.bringNext();
    this.resume();

    this.keydownSubscription && this.keydownSubscription.unsubscribe();
    this.keydownSubscription = this.keyBuffer.start()
      .keydown.subscribe(this.onKeyDown.bind(this));
    this.intervalHandle && clearInterval(this.intervalHandle);
    this.intervalHandle = setInterval(() => this.tick(), 200);

  }

  end() {
    this.gameOver = true;
    this.keyBuffer.stop();
    this.keydownSubscription && this.keydownSubscription.unsubscribe();
    this.intervalHandle && clearInterval(this.intervalHandle);
  }

  enableAutoFall() {
    let self = this;

    (function autoFall() {
      self.disableAutoFall();
      if (
        self._autoFallHandle && !self.paused &&
        !(self.autoMoving && self.lastKey === "ArrowDown")
      ) {
        self.move(Direction.Down);
      }
      self._autoFallHandle = setTimeout(autoFall, self.speed);
    })();

  }

  disableAutoFall() {
    this._autoFallHandle && clearTimeout(this._autoFallHandle);
  }


  pause() {
    this.keyBuffer.stop();
    this.paused = true;
  }

  resume(bringNext?: boolean) {
    this.paused = false;
    this.keyBuffer.start();
    if (bringNext) { this.bringNext(); }
  }

  private _shiftQueue() {
    if (!this.inQueue.length) { //Initialization
      this.next = this.pieceBag.getNext();
      for (let i = 4; i--;) { this.inQueue.push(this.pieceBag.getNext()); }
    }
    let curr = this.next;
    this.next = this.inQueue.shift();
    this.inQueue.push(this.pieceBag.getNext());
    return curr;
  }

  bringNext() {
    this.swapped = null;
    this.disableAutoFall();//TODO:?
    if (!this.current.reset(this._shiftQueue(), this.matrixCmp.matrix)) {
      this.end();
    }
    this.enableAutoFall();//TODO:?
  }

  holdCurrent() {
    if (this.swapped) {
      this.swapFoul = true;
      setTimeout(() => this.swapFoul = null, 800);
      return;
    }
    this.swapped = true;
    let inHold = this.inHold;
    this.inHold = this.current.shape;

    if (inHold) {
      this.current.reset(inHold, this.matrixCmp.matrix);
    } else {
      this.bringNext();
    }
  }

  tick() {
    if (this.paused) { return; }
    if (this.landed) {
      this.landed = null;
      this.paused = true;
      let lineCnt = this.matrixCmp.clearLines();
      this.score += lineCnt * 100;
      this.lines += lineCnt;
      if (lineCnt) { this.matrixCmp.tighten(); }
      this.paused = null;
    }
    // if (this.keyBuffer.keyHodling) { this.onKeyDown(this.keyBuffer.keyHodling) }


  }

  onKeyDown(key: string) {
    if (this.paused) { return; }
    switch (key) {
      case "ArrowDown":
      case "ArrowLeft":
      case "ArrowRight":
      case "Down":       //IE11 Quirk!
      case "Left":
      case "Right":
        this.lastKey = key;
        this.move(<Direction>(key[0] === 'A' ? key.substr(5) : key));
        this.enableAutoMove();
        break;
      case "ArrowUp":
      case "Up":
      case "x":
        return this.rotate(Rotation.Clockwise);
      case "z":
        return this.rotate(Rotation.Counterclockwise);
      case " ":
      case "SpaceBar":
        return this.hardDrop();
      case "Shift":
        return this.holdCurrent();
      default:
        return;
    }

  }

  enableAutoMove() {
    this._autoMoveHandle && clearTimeout(this._autoMoveHandle);
    this._autoMoveHandle = setTimeout(() => {
      if (this.lastKey && this.lastKey === this.keyBuffer.keyHodling) {
        this.autoMoving = true;
        this.onKeyDown(this.lastKey);
      } else {
        this.autoMoving = null;
      }
    }, this._autoMoveTick * (this.autoMoving ? 1 : 2));
  }

  rotate(rot: Rotation) {
    let ok = this.current.tryRotate(rot, this.matrixCmp.matrix);
    if (ok) {
      //TODO:T-spin?
    }
  }

  move(dir: Direction) {
    let ok = this.current.tryMove(dir, this.matrixCmp.matrix);
    if (!ok && dir === Direction.Down) {
      this._landTetrimino();
    }
  }

  hardDrop() {
    this.current.hardDrop(this.matrixCmp.matrix);
    this._landTetrimino();
    console.log('Hard drop');
  }

  private _landTetrimino() {
    this.landed = true;
    let outline = this.current.getOutline();
    if (outline.coords.every(c => c[1] < 3)) { return this.end(); }//LockOut
    this.matrixCmp.lockTetrimino(this.current.getOutline());
    if (this.matrixCmp.matrix[1].some(c => !!c)) { return this.end(); }//PileOut
    this.bringNext();
    //TODO: waiting for event triggered by matrix
  }



}